package tjo;

import java.awt.Color;
import robocode.HitRobotEvent;
import robocode.HitWallEvent;
import robocode.Robot;
import robocode.Rules;
import robocode.ScannedRobotEvent;

/**
 * This robot moves in a straight line and turns its gun to face an enemy as it drives.
 * 
 * @author Jordan Takayama
 * @since 10/05/2011
 */
public class Meteor extends Robot {
  /**
   * Controls whether the robot moves forward or back. Based on:
   * <http://mark.random-article.com/weber/java/robocode/lesson5.html>, October 5, 2011.
   */
  private byte moveDirection = 1;

  /**
   * Used to move the radar in one direction or another.
   */
  private byte radarDirection = 1;

  /**
   * The origin (0.0, 0.0) of the Robocode coordinate system.
   */
  private static final double ORIGIN = 0.0;

  /**
   * The maximum possible x-coordinate of the battlefield.
   */
  private double arenaWidth;

  /**
   * The maximum possible y-coordinate of the battlefield.
   */
  private double arenaHeight;

  /**
   * Defines the "danger zone" (in pixels) around the walls which determines if the robot is near a
   * wall or corner. (This robot usually moves ~100 pixels at a time, so this seemed reasonable.)
   */
  private static final double outerBounds = 200.0;

  /**
   * The name of the robot currently being targeted.
   */
  private String currentTarget = null;

  /**
   * Turns since the current target was last seen.
   */
  private int scansSinceTargetSeen = 0;

  /**
   * True or false based on whether or not this robot has a lock on a target.
   */
  private boolean hasLock = false;

  private boolean sawTarget = false;

  /**
   * The run method for the Meteor robot.
   */
  @Override
  public void run() {
    // Set colors.
    setBodyColor(new Color(255, 32, 32));
    setGunColor(new Color(234, 77, 0));
    setRadarColor(new Color(210, 0, 0));
    setScanColor(Color.red);
    setBulletColor(Color.black);

    arenaWidth = getBattleFieldWidth();
    arenaHeight = getBattleFieldHeight();

    // Make sure the gun turns independently of the robot.
    setAdjustGunForRobotTurn(true);
    while (true) {
      if (this.getVelocity() == 0) {
        avoidBounds(ORIGIN, arenaWidth, arenaHeight, getX(), getY(), outerBounds);
        ahead(100.0 * moveDirection);
      }
      if (hasLock) {
        turnRadarRight(22.5 * radarDirection);
        if (!sawTarget) {
          scansSinceTargetSeen++;
          radarDirection *= -1;
          turnRadarRight(45.0 * radarDirection);
        }
        if (scansSinceTargetSeen == 2) {
          hasLock = false;
        }
      }
      else {
        turnRadarRight(180.0 * radarDirection);
      }
      ahead(100.0 * moveDirection);
    }
  }

  /**
   * Triggered when the robot's radar scans another robot.
   * 
   * Turns so that the robot is perpendicular to the other robot, then fires.
   * 
   * @param event The ScannedRobotEvent set by the game.
   */
  @Override
  public void onScannedRobot(ScannedRobotEvent event) {
    double result;
    double eventBearing = event.getBearing();
    double startHeading = this.getHeading();
    double xAfterMove, yAfterMove;
    if (currentTarget == null) {
      currentTarget = event.getName();
      hasLock = true;
    }
    else if (currentTarget.equals(event.getName())) {
      scansSinceTargetSeen = 0;
      sawTarget = true;
    }
    else {
      scansSinceTargetSeen++;
      sawTarget = false;
      if (scansSinceTargetSeen == 2) {
        currentTarget = null;
        hasLock = false;
      }
    }
    turnRight(eventBearing + 90.0 - (10 * moveDirection));
    if (eventBearing != 0.0) {
      turnGunRight(startHeading - this.getGunHeading() + eventBearing);
    }
    fire(calculateShotPower(event.getDistance()));
    xAfterMove = newX(100.0 * moveDirection, this.getHeading());
    yAfterMove = newY(100.0 * moveDirection, this.getHeading());
    result =
        distanceToWallOrCorner(ORIGIN, arenaWidth, arenaHeight, xAfterMove, yAfterMove, 
        outerBounds);
    if (result == -1.0) {
      ahead(100.0 * moveDirection);
    }
    else {
      avoidBounds(ORIGIN, arenaWidth, arenaHeight, xAfterMove, yAfterMove, outerBounds);
      ahead(100.0 * moveDirection);
    }
  }

  /**
   * Triggered when the robot hits another robot.
   * 
   * @param event The hit-robot event set by the game.
   */
  @Override
  public void onHitRobot(HitRobotEvent event) {
    // Turn gun towards other robot and attack
    turnGunRight(this.getHeading() - this.getGunHeading() + event.getBearing());
    fire(Rules.MAX_BULLET_POWER);
    // Turn 90 degrees away to evade, then drive away.
    turnRight((this.getGunHeading() + 90.0) - this.getHeading());
    ahead(100 * moveDirection);
  }

  /**
   * Triggered when the robot hits a wall.
   * 
   * @param event The hit-wall event set by the game.
   */
  @Override
  public void onHitWall(HitWallEvent event) {
    turnRight(90.0);
    ahead(100 * moveDirection);
  }

  /**
   * Calculates the power that a shot should be fired with, decreasing as distance increases.
   * 
   * @param distance The distance between this robot and some targeted robot.
   * @return The power of a shot fired at a target located at the given distance away from this
   * robot. Shot power is proportional to the distance "d" to a target, as follows:
   * <ul>
   * <li>In the format &quot;Distance (pixels): Bullet Power&quot;</li>
   * <li>d < 100.0 : Rules.MAX_BULLET_POWER</li>
   * <li>100.0 <= d < 300.0 : 2.0 + (1.0 / (0.01 * distance))</li>
   * <li>300.0 <= d < 500.0 : 1.0 + (1.0 / (0.01 * distance))</li>
   * <li>500.0 <= d : 0.5 + (2.0/(0.01 * d))</li>
   * <li>getEnergy() < 15.0 : 0.5 + (2.0/(0.01 * d))</li>
   * </ul>
   */
  public double calculateShotPower(double distance) {
    double shotPower = 0.0;
    if (distance >= 500.0 || getEnergy() < 15.0) {
      shotPower = 0.5 + (2.0 / (0.01 * distance));
    }
    else if (distance < 100.0) {
      shotPower = Rules.MAX_BULLET_POWER;
    }
    else if (distance >= 100.0 && distance < 300.0) {
      shotPower = 2.0 + (1.0 / (0.01 * distance));
    }
    else if (distance >= 300.0 && distance < 500.0) {
      shotPower = 1.0 + (1.0 / (0.01 * distance));
    }
    return shotPower;
  }

  /**
   * A method that turns the robot to avoid a wall or corner.
   * 
   * This method calls the whichWallOrCorner method to determine which wall or corner the robot is
   * close to, then turns away from it.
   * 
   * @param origin The origin of a coordinate system.
   * @param xMax The maximum possible x-value in a coordinate system.
   * @param yMax The maximum possible y-value in a coordinate system.
   * @param xVal The x-coordinates being checked.
   * @param yVal The y-coordinates being checked.
   * @param limit A robot is close to a wall or corner if it is within this many pixels of it.
   */
  public void avoidBounds(double origin, double xMax, double yMax, double xVal, double yVal,
      double limit) {
    double currentHeading = this.getHeading();
    int nearBound = whichWallOrCorner(origin, xMax, yMax, xVal, yVal, limit);
    if (nearBound != 0) {
      // If the robot is approaching a wall roughly head-on, this will turn it well away:
      // North wall: 0 + 105 = 105 degrees, East wall: 90 + 105 = 195 degrees,
      // South wall: 180 + 105 = 285 degrees, West wall: 270 + 105 degrees = 375 % 360 = 15 degrees.
      if (nearBound <= 4) {
        turnRight(105.0);
      }
      // If the robot is approaching a corner, turn so that the robot points 105.0 degrees away from
      // the corner (e.g., the south-west corner is at 225 degrees, so the robot must turn until it
      // is angled at 330.0 degrees). Cases where heading == the desired angle are not covered,
      // as in those cases the robot does not need to turn at all.
      else if (nearBound == 5) {
        if (currentHeading < 330.0) {
          turnRight(330.0 - currentHeading);
        }
        else if (currentHeading > 330.0) {
          turnLeft(currentHeading - 330.0);
        }
      }
      else if (nearBound == 6) {
        if (currentHeading < 240.0) {
          turnRight(240.0 - currentHeading);
        }
        else if (currentHeading > 240.0) {
          turnLeft(currentHeading - 240.0);
        }
      }
      else if (nearBound == 7) {
        if (currentHeading < 150.0) {
          turnRight(150.0 - currentHeading);
        }
        else if (currentHeading > 150.0) {
          turnLeft(currentHeading - 150.0);
        }
      }
      else if (nearBound == 8) {
        if (currentHeading < 60.0) {
          turnRight(60.0 - currentHeading);
        }
        // 315 degrees (the northwest corner) + 105 degrees exceeds 360 degrees.
        // Therefore this case handles any heading between 315.0 and 15.0 degrees.
        else if (currentHeading < 420.0) {
          turnLeft(420.0 - currentHeading);
        }
      }
    }
  }

  /**
   * Determines which, if any, corners or walls the robot is getting dangerously close to.
   * 
   * The use of variables for all coordinates allows the user to define different bounds for robot
   * movement within the standard coordinates, if desired. This method assumes it will never be true
   * that a robot is within [limit] for any two parallel walls simultaneously, or that a robot's
   * position is within [limit] for more than two walls at a time.
   * 
   * @param origin The origin of a coordinate system. It is assumed that origin == 0.0.
   * @param xMax The maximum possible x-value in a coordinate system. It is assumed that xMax ==
   * getBattlefieldWidth().
   * @param yMax The maximum possible y-value in a coordinate system. It is assumed that yMax ==
   * getBattlefieldHeight().
   * @param xVal The x-coordinates being checked.
   * @param yVal The y-coordinates being checked.
   * @param limit A robot is close to a wall or corner if it is within this many pixels of it.
   * @return 0 if not a wall or corner; 1 if west wall; 2 if south wall; 3 if east wall; 4 if north
   * wall; 5 if south-west corner; 6 if south-east corner; 7 if north-east corner; 8 if north-west
   * corner.
   */
  public int whichWallOrCorner(double origin, double xMax, double yMax, double xVal, double yVal,
      double limit) {
    int result = 0;
    boolean westFlag = false;
    boolean eastFlag = false;
    boolean northFlag = false;
    boolean southFlag = false;
    // Cases to set wall flags.
    if ((xVal - origin) <= limit) {
      westFlag = true;
    }
    if ((yVal - origin) <= limit) {
      southFlag = true;
    }
    if ((xMax - xVal) <= limit) {
      eastFlag = true;
    }
    if ((yMax - yVal) <= limit) {
      northFlag = true;
    }

    // Once flags are set, set the return value, checking the more specific corner cases first.
    if (southFlag && westFlag) {
      result = 5;
    }
    else if (southFlag && eastFlag) {
      result = 6;
    }
    else if (northFlag && eastFlag) {
      result = 7;
    }
    else if (northFlag && westFlag) {
      result = 8;
    }
    // Check the wall cases only if the position is not known to be near a corner.
    else {
      if (westFlag) {
        result = 1;
      }
      else if (southFlag) {
        result = 2;
      }
      else if (eastFlag) {
        result = 3;
      }
      else if (northFlag) {
        result = 4;
      }
    }
    return result;
  }

  /**
   * Calculates distance to nearest wall or corner, if the robot is near one.
   * 
   * Distance is based on a straight line drawn to the wall/corner from the current position, and
   * ignores heading.
   * 
   * @param origin The origin of a coordinate system. It is assumed that origin == 0.0.
   * @param xMax The maximum possible x-value in a coordinate system. It is assumed that xMax ==
   * getBattlefieldWidth().
   * @param yMax The maximum possible y-value in a coordinate system. It is assumed that yMax ==
   * getBattlefieldHeight().
   * @param xVal The x-coordinates being checked.
   * @param yVal The y-coordinates being checked.
   * @param limit A robot is close to a wall or corner if it is within this many pixels of it.
   * @return The distance to the nearest wall or corner, or -1.0 if the robot is not within [limit]
   * of a wall or corner.
   */
  public double distanceToWallOrCorner(double origin, double xMax, double yMax, double xVal,
      double yVal, double limit) {
    // Set default result
    double result = -1.0;
    int wallOrCorner = whichWallOrCorner(origin, xMax, yMax, xVal, yVal, limit);
    if (wallOrCorner > 0) {
      if (wallOrCorner == 1) {
        result = distance(xVal, yVal, origin, yVal);
      }
      else if (wallOrCorner == 2) {
        result = distance(xVal, yVal, xVal, origin);
      }
      else if (wallOrCorner == 3) {
        result = distance(xVal, yVal, xMax, yVal);
      }
      else if (wallOrCorner == 4) {
        result = distance(xVal, yVal, xVal, yMax);
      }
      else if (wallOrCorner == 5) {
        result = distance(xVal, yVal, origin, origin);
      }
      else if (wallOrCorner == 6) {
        result = distance(xVal, yVal, xMax, origin);
      }
      else if (wallOrCorner == 7) {
        result = distance(xVal, yVal, xMax, yMax);
      }
      else if (wallOrCorner == 8) {
        result = distance(xVal, yVal, origin, yMax);
      }
    }
    return result;
  }

  /**
   * Calculates the distance between two points.
   * 
   * @param currentX The x-coordinate of the point representing the robot's current position.
   * @param currentY The y-coordinate of the point representing the robot's current position.
   * @param otherX The x-coordinate of the second point.
   * @param otherY The y-coordinate of the second point.
   * @return The distance between the two points.
   */
  private double distance(double currentX, double currentY, double otherX, double otherY) {
    double xSquared = Math.pow((otherX - currentX), 2.0);
    double ySquared = Math.pow((otherY - currentY), 2.0);
    return Math.sqrt(xSquared + ySquared);
  }

  /**
   * Calculates an x-coordinate based on the distance to be moved and the robot's current heading.
   * 
   * @param distanceToMove The distance the robot will be moving.
   * @param currentHeading The robot's current heading.
   * @return the x-coordinate of the robot's estimated position after moving.
   */
  public double newX(double distanceToMove, double currentHeading) {
    double result;
    if (distanceToMove < 0) {
      result = Math.cos(Math.toRadians(currentHeading + 180.0)) * Math.abs(distanceToMove);
    }
    else {
      result = Math.cos(Math.toRadians(currentHeading)) * distanceToMove;
    }
    return result;
  }

  /**
   * Calculates a y-coordinate based on the distance to be moved and the robot's current heading.
   * 
   * @param distanceToMove The distance the robot will be moving.
   * @param currentHeading The robot's current heading.
   * @return the y-coordinate of the robot's estimated position after moving.
   */
  public double newY(double distanceToMove, double currentHeading) {
    double result;
    if (distanceToMove < 0) {
      result = Math.sin(Math.toRadians(currentHeading + 180.0)) * Math.abs(distanceToMove);
    }
    else {
      result = Math.sin(Math.toRadians(currentHeading)) * distanceToMove;
    }
    return result;
  }
}
